package com.pinfly.common.http;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.Properties;

import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.cert.X509Certificate;

import org.apache.log4j.Logger;

import com.pinfly.common.config.ConfigException;
import com.pinfly.common.config.PfyProperties;
import com.pinfly.common.config.PfyPropertiesFactory;

/**
 * Implements a server to process Https requests.
 * <p>
 * This class extends <code>HttpServer</code> to handle Https requests.
 * <p>
 * For information on how to use the class to construct and Https server see
 * {@link HttpServer HttpServer}.
 */
public class HttpsServer extends HttpServer
{
    private static final Logger logger = Logger.getLogger (HttpServer.class);

    /** Default port to listent on */
    public static final int DEF_PORT = 443;
    /** Default server name */
    public static final String DEF_NAME = "HttpsServer";

    private static PfyProperties s_props = null;
    private static final String[] cipherSuites =
    {
    // SSL V3 with RSA authentication, triple DES (112-bit strong)
    // block encryption; and a Secure Hash Algorithm checksum
    // This is the cipher suite specified by CCOW.
    "SSL_RSA_WITH_3DES_EDE_CBC_SHA" };

    private ServerSocketFactory m_socketFactory;
    private boolean m_clientAuth = false;
    private String[] m_validClientIds;

    static
    {
        try
        {
            s_props = PfyPropertiesFactory.getProperties ("ssl");
        }
        catch (ConfigException e)
        {
            logger.warn (e.getMessage ());
        }
    }

    /**
     * Create an HTTPS server with a default name, on the default port, with a
     * default request processor. The default request processor merely prints
     * the request parameters as debug output.
     */
    public HttpsServer ()
    {
        super (DEF_NAME, getDefaultProcessor (), DEF_PORT);
        init ();
    }

    /**
     * Create an HTTPS server with the specified name and request processor. The
     * default port will be used.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     */
    public HttpsServer (String name, RequestProcessor processor)
    {
        super (name, processor, DEF_PORT);
        init ();
    }

    /**
     * Create an HTTPS server with the specified name, listening on the
     * specified port and with the specified request processor.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     * @param port The port on which to listen for requests. A value of 0 will
     *            let the underlying operating system determine the port number.
     *            The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     */
    public HttpsServer (String name, RequestProcessor processor, int port)
    {
        super (name, processor, port);
        init ();
    }

    /**
     * Create an HTTPS server with the specified name, listening on the
     * specified port and with the specified request processor.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     * @param port The port on which to listen for requests. A value of 0 will
     *            let the underlying operating system determine the port number.
     *            The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     * @param queueSize The serversocket backlog size if the parameter is less
     *            than or equal to 0 the system default of 50 is used.
     */
    public HttpsServer (String name, RequestProcessor processor, int port, int queueSize)
    {
        super (name, processor, port, queueSize);
        init ();
    }

    /** Perform some initialization needed by all the constructors */
    private void init ()
    {
        setServerProperties (m_serviceName);
    }

    /**
     * Controls whether the connections which are accepted must include client
     * authentication. By default, clients do not need to provide authentication
     * information. Setting the <code>ClientAuth</code> property in the
     * ssl.properties file can change the default. This method overrides both
     * the default value and any value specified in the properties file.
     * <p>
     * Note that any change to this value will not take affect until the next
     * time the server is started.
     * 
     * @param flag if <code>true</code> then client authentication is required
     */
    public void setNeedClientAuth (boolean flag)
    {
        m_clientAuth = flag;
    }

    /**
     * Does the server require client authentication?
     * 
     * @return <code>true</code> if the server requires client authentication;
     *         else <code>false</code>.
     */
    public boolean getNeedClientAuth ()
    {
        return m_clientAuth;
    }

    /**
     * Set server properties from the ssl.properties file. This method is
     * equivalent to setServerProperties (props, name), where props refers to
     * the ssl.properties file.
     * 
     * @param name The name to use in lookup up the properties
     * @throws IllegalArgumentException if the ssl.properties file doesn't
     *             contain all required properties.
     * @see #setServerProperties(MSProperties,String)
     */
    public void setServerProperties (String name)
    {
        setServerProperties (s_props, name);
    }

    /**
     * Set server properties from a properties file.
     * <p>
     * When retrieving values from the properties file, a search is first made
     * for &lt;name&gt;.&lt;property&gt;. If that is not found, then a search is
     * made for *.&lt;property&gt;. If that is not found and a default value is
     * defined it is used. If no value is specified and no default is defined,
     * an InvalidArgumentException is thrown.
     * <p>
     * The properties are
     * <table cellSpacing="0" cellPadding="3" border="1">
     * <tr class="TableHeadingColor" bgColor="#ccccff">
     * <td><b>Property </b></td>
     * <td><b>Description </b></td>
     * <td><b>Default </b></td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>KeyStoreFile</td>
     * <td>The name of the keystore file containing the certificate and
     * corresponding private key.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>PassPhrase</td>
     * <td>The pass phrase fro retrieving the private key from the keystore.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>CertAlias</td>
     * <td>The alias that refers to the certificate and key in the keystore
     * file.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>ClientAuth</td>
     * <td>Should the certificate be requested from the client</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>PeerId</td>
     * <td>The valid client id.</td>
     * <td>any client</td>
     * </tr>
     * </table>
     * 
     * @param props The properties to use.
     * @param name The name to use in looking up the properties.
     * @throws IllegalArgumentException if the properties file doesn't contain
     *             all required properties
     * @see #setServerProperties
     */
    public void setServerProperties (Properties props, String name)
    {

        if (props == null)
        {
            throw new IllegalArgumentException ("props cannot be null");
        }

        // First get the require properties that define the certificate
        // and set up the socket factory to use that certificate.
        String keyStoreFile = SSLUtil.getProp (props, SSLUtil.KEY_STORE_FILE, name, null);
        String passPhrase = SSLUtil.getProp (props, SSLUtil.PASS_PHRASE, name, null);
        String certAlias = SSLUtil.getProp (props, SSLUtil.CERT_ALIAS, name, null);
        if (keyStoreFile == null || passPhrase == null || certAlias == null)
        {
            throw new IllegalArgumentException ("Properties file does not contain required properties: "
                                                + SSLUtil.KEY_STORE_FILE + ", " + SSLUtil.PASS_PHRASE + ", "
                                                + SSLUtil.CERT_ALIAS + " for server " + name + '.');
        }
        try
        {
            SSLContext context = SSLUtil.getSSLContext (keyStoreFile, passPhrase, certAlias);
            m_socketFactory = context.getServerSocketFactory ();
        }
        catch (Exception e)
        {
            // Now what - no socket factory
            logger.fatal ("Couldn't create SecureServerSocketFactory for " + name, e);
        }

        // Now the optional items
        String auth = SSLUtil.getProp (props, "ClientAuth", name, null);
        if (auth != null)
        {
            setNeedClientAuth (auth.equalsIgnoreCase ("true"));
        }

        String clientId = SSLUtil.getProp (props, "PeerId", name, null);
        if (clientId != null)
        {
            setClientId (clientId);
        }
    }

    /**
     * Set server properties from a properties file.
     * <p>
     * When retrieving values from the properties file, a search is first made
     * for &lt;name&gt;.&lt;property&gt;. If that is not found, then a search is
     * made for *.&lt;property&gt;. If that is not found and a default value is
     * defined it is used. If no value is specified and no default is defined,
     * an InvalidArgumentException is thrown.
     * <p>
     * The properties are
     * <table cellSpacing="0" cellPadding="3" border="1">
     * <tr class="TableHeadingColor" bgColor="#ccccff">
     * <td><b>Property </b></td>
     * <td><b>Description </b></td>
     * <td><b>Default </b></td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>KeyStoreFile</td>
     * <td>The name of the keystore file containing the certificate and
     * corresponding private key.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>PassPhrase</td>
     * <td>The pass phrase fro retrieving the private key from the keystore.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>CertAlias</td>
     * <td>The alias that refers to the certificate and key in the keystore
     * file.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>ClientAuth</td>
     * <td>Should the certificate be requested from the client</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>PeerId</td>
     * <td>The valid client id.</td>
     * <td>any client</td>
     * </tr>
     * </table>
     * 
     * @param props The properties to use.
     * @param name The name to use in looking up the properties.
     * @throws IllegalArgumentException if the properties file doesn't contain
     *             all required properties
     * @see #setServerProperties
     */
    public void setServerProperties (PfyProperties props, String name)
    {
        // First get the require properties that define the certificate
        // and set up the socket factory to use that certificate.
        String keyStoreFile = SSLUtil.getProp (props, SSLUtil.KEY_STORE_FILE, name, null);
        String passPhrase = SSLUtil.getProp (props, SSLUtil.PASS_PHRASE, name, null);
        String certAlias = SSLUtil.getProp (props, SSLUtil.CERT_ALIAS, name, null);
        if (keyStoreFile == null || passPhrase == null || certAlias == null)
        {
            throw new IllegalArgumentException ("Properties file does not contain required properties: "
                                                + SSLUtil.KEY_STORE_FILE + ", " + SSLUtil.PASS_PHRASE + ", "
                                                + SSLUtil.CERT_ALIAS + " for server " + name + '.');
        }
        try
        {
            SSLContext context = SSLUtil.getSSLContext (keyStoreFile, passPhrase, certAlias);
            m_socketFactory = context.getServerSocketFactory ();
        }
        catch (Exception e)
        {
            // Now what - no socket factory
            logger.fatal ("Couldn't create SecureServerSocketFactory for " + name, e);
        }

        // Now the optional items
        String auth = SSLUtil.getProp (props, "ClientAuth", name, null);
        if (auth != null)
        {
            setNeedClientAuth (auth.equalsIgnoreCase ("true"));
        }

        String clientId = SSLUtil.getProp (props, "PeerId", name, null);
        if (clientId != null)
        {
            setClientId (clientId);
        }
    }

    /**
     * Set the list of valid client ids. The list of ids provided completely
     * replaces any previous list. Each entry in the list specifies a value
     * which may be present in the subject field of the client's certificate.
     * Each entry should be of the form name=value, where name is one of the
     * fields in an X500 distinguished name, and value is the value that the
     * field should have. For example <code>ou=CCOW.ContextManager</code>.
     * <p>
     * A client passes validation if any of the specified ids are in its subject
     * field.
     * 
     * @param ids The set of valid client ids
     */
    public void setClientIds (String[] ids)
    {
        if (ids != null && ids.length == 0)
            ids = null;
        m_validClientIds = ids;
        if (m_validClientIds != null)
        {
            setNeedClientAuth (true);
            for (int i = 0; i < m_validClientIds.length; i++)
            {
                m_validClientIds[i] = SSLUtil.validateId (m_validClientIds[i]);
            }
        }
    }

    /**
     * Set a valid client id. This is a convenience method equivalent to <code>
     * 	setClientIds (new String[] {id})</code>.
     * 
     * @param id The valid client id
     * @see #setClientIds
     */
    public void setClientId (String id)
    {
        setClientIds (new String[]
        { id });
    }

    /**
     * Validate and return information about the client connection. This
     * implementation returns a ClientConnection instance which shows that the
     * client does use SSL. If a client certificate chain was provided, that is
     * also included in the ClientConnection. If client validation was requested
     * but the validation failse, then an MSHttpException is thrown.
     */
    ClientConnection getClientInfo (Socket s) throws MSHttpException
    {
        X509Certificate[] chain = null;

        if (m_validClientIds != null)
        {
            // Get the SSL session from the socket.
            // Conveniently, this call will hang until the handshake
            // is completed, so we don't have to use a handshake completed
            // listener.
            SSLSession session = ((SSLSocket) s).getSession ();

            // Get the cert from the session and validate it.
            try
            {
                chain = session.getPeerCertificateChain ();
                SSLUtil.validateCert (chain[0], m_validClientIds);
            }
            catch (SSLPeerUnverifiedException e)
            {
                // The client didn't give us a certificate
                throw new MSHttpException ("No client cert provided");
            }
        }

        return new ClientConnection (s, true, chain);

    }

    /**
     * Create an SSL server socket on the specified port.
     * 
     * @param portNumber the port number to associate with the socket. A value
     *            of 0 will let the underlying operating system determine the
     *            port number. The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     * @return a <code>ServerSocket</code> listening for requests on the
     *         specified port. Returns <code>null</code> if there is an error
     *         creating the socket.
     * @throws IllegalArgumentException if there is an error creating the socket
     */
    protected ServerSocket createSocket (int portNumber)
    {
        SSLServerSocket ss = null;

        try
        {
            // set up server socket
            if (getIncomingQueueSize () > 0)
            {
                ss = (SSLServerSocket) m_socketFactory.createServerSocket (portNumber, getIncomingQueueSize ());
            }
            else
            {
                ss = (SSLServerSocket) m_socketFactory.createServerSocket (portNumber);
            }

            // set socket options
            ss.setEnabledCipherSuites (cipherSuites);
            ss.setNeedClientAuth (m_clientAuth);

            return ss;
        }
        catch (Exception e)
        {
            logger.fatal ("Failed to create server socket on port " + portNumber + ".  " + e.getMessage (), e);
            String msg = e.getMessage ();
            if (msg == null || msg.length () == 0)
            {
                msg = e.getClass ().getName ();
            }
            throw new IllegalArgumentException ("Failed to create socket due to " + msg);
        }
    }

}
